package com.frontalcode 
{
	import flash.net.*;
	import flash.events.*;
	import flash.utils.*;

	/**
	 * The ContainerForm class handles the <form /> tag.
	 */
	public class ContainerForm extends Container
	{
		static public var activeFocusMgr : *;	// IFocusManager;
		static public var stageFocusMgr : *;	// IFocusManager;
		public var focusMgr : *;				// IFocusManager;
		public var initialFocus : ContainerInput = null;
		private var _defaultButton : ContainerInput = null;
		internal var multiFileSubmissions : Object = { };
		internal var activeSubmissions : Dictionary = new Dictionary ( );
		internal var archivedSubmissions : Dictionary = new Dictionary ( );
		
		/**
		 * Creates a new ContainerForm instance.
		 * 
		 * @param document	The document this element belongs to.
		 * @param parent	The element's parent in the document tree.
		 * @param node		The XML node this element is associated with.
		 * @param options	Options affecting the behavior of this instance.
		 * 					(This object does not have built-in support for any
		 * 					options.)
		 */
		public function ContainerForm ( document : Document, parent : DocumentElement, node : XML, options : Object = null )
		{
			super ( document, parent, node, options );
			
			addListener ( this, FrontalEvent.SUBMIT, submitHandler, false, -1000 );
		}
		
		/**
		 * @private
		 */
		override public function show ( force : * = null ) : void
		{
			// FocusManager is not happy about deactivating / activating without
			// a stage so we try to do these things before that occurs.
			//
			if ( force === false || ( force === null && getStyle ( "display" ) == "none" && getStyle ( "visibility" ) == "hidden"  ) ) deactivate ( );
			super.show ( force );
		}
		
		/**
		 * @private
		 */
		override public function applyMovieStyles ( ) : void
		{
			super.applyMovieStyles ( );
			
			if ( focusMgr == null && movie != null && movie.stage != null ) 
			{
				try
				{
					var focusMgrClass : Class = document.externalAssetManager.getDefinition ( "com.frontalcode.FrontalFocusManager" ) as Class;
					focusMgr = new focusMgrClass ( movie );
					if ( defaultButton != null ) focusMgr.defaultButton = defaultButton.input as ( document.externalAssetManager.getDefinition ( "fl.controls.Button" ) as Class );
					focusMgr.deactivate ( );
				}
				catch ( e : Error )
				{
				}
			}
		}
		
		/**
		 * The default Button for the form.
		 */
		public function set defaultButton ( value : ContainerInput ) : void
		{
			var buttonClass : Class = document.externalAssetManager.getDefinition ( "fl.controls.Button" ) as Class;
			if ( value.input is buttonClass )
			{
				_defaultButton = value;
				if ( focusMgr != null ) focusMgr.defaultButton = value.input as buttonClass;
			}
			else
			{
				Debugger.logMessage ( Debugger.ERROR, "ContainerForm", "FocusManager.defaultButton must be an instance of the Button class" );
			}
		}
		
		public function get defaultButton ( ) : ContainerInput { return _defaultButton; }
		
		/**
		 * @private
		 */
		public function activate ( ) : void
		{
			if ( focusMgr != null )
			{
				if ( activeFocusMgr != null && activeFocusMgr !== focusMgr ) activeFocusMgr.deactivate ( );
				var FocusManager : Class = document.externalAssetManager.getDefinition ( "fl.managers.FocusManager" ) as Class;
				if ( stageFocusMgr != null && stageFocusMgr is FocusManager && ( stageFocusMgr as FocusManager).form.stage != null ) stageFocusMgr.deactivate ( );
				focusMgr.activate ( );
				
				activeFocusMgr = focusMgr;
			}
		}
		
		/**
		 * @private
		 */
		public function deactivate ( ) : void
		{
			if ( focusMgr != null )
			{
				focusMgr.deactivate ( );
				
				if ( activeFocusMgr === focusMgr ) 
				{
					activeFocusMgr = null;
					var FocusManager : Class = document.externalAssetManager.getDefinition ( "fl.managers.FocusManager" ) as Class;
					if ( stageFocusMgr != null && stageFocusMgr is FocusManager && ( stageFocusMgr as FocusManager).form.stage != null ) stageFocusMgr.activate ( );
				}
			}
		}
		
		/**
		 * This method submits the form. It does this circuitously though.
		 * First, it submits a FrontalEvent.SUBMIT event for which the form
		 * has a low (-1000) priority event handler. If the handler
		 * receives the event and event.isDefaultPrevented ( ) is false
		 * then it run the onSubmit interaction on the form tag and if true
		 * is returned then the method doSubmit is called. This
		 * architecture allows for submit event to be handled in custom
		 * ways and allows for validation via the onSubmit interaction.
		 */
		public function submit ( ) : void { dispatchEvent ( new FrontalEvent ( FrontalEvent.SUBMIT ) ); }
		
		/*
			Function: submitHandler
			
				The event handler for the FrontalEvent.SUBMIT event sent by the 
				submit method. See the description of that method for more
				details.
		*/	
		/**
		 * @private
		 */
		public function submitHandler ( event : FrontalEvent ) : void 
		{ 
			if ( ! event.isDefaultPrevented ( ) )
			{
				var value : Object = runInteractions ( "onSubmit", { event: event } );
				if ( value !== false ) doSubmit ( );
			}
		}
		
		/*
			Function: doSubmit
			
				This method gathers the values of the inputs and sends them
				according to the form's action attribute. To support the
				onSubmit interaction, do not call this method directly. Instead
				call submit.
		*/	
		/**
		 * @private
		 */
		public function doSubmit ( ) : void
		{
			var variables : URLVariables = new URLVariables ( );
			var fileReferences : Array = [ ];
			var hasFileInputs : Boolean = false;
			
			// Find all our inputs which are either input or text tags
			// with the style flash-text-type set to "input."
			//
			for each ( var inputNode : XML in node.descendants ( "input" ) + node.descendants ( "text" ) )
			{
				var input : DocumentElement = document.getNodeElement ( inputNode );		
				if ( input == null || ( inputNode.name ( ) == "text" && input.getStyle ( "flash-text-type" ) != "input" ) ) continue;
				
				var name : String = input.getAttribute ( "name" );
				if ( name == null ) continue;
				
				var value : *;
				
				if ( input is ContainerInput )
				{
					value = input.runInteractions ( "getSubmissionValue" );
					if ( value === undefined ) value = ( input as ContainerInput ).value;
					if ( ! hasFileInputs ) hasFileInputs = input.gA ( "type" ) == "file";
				}
				
				if ( value !== undefined )
				{
					if ( value is FileReferenceList )
					{
						hasFileInputs = true;
						var list : FileReferenceList = value as FileReferenceList;
						if ( list.fileList != null ) for ( var i : uint = 0; i < list.fileList.length; i++ ) fileReferences.push ( { name: name, file: list.fileList [ i ] } );
					}
					else if ( value is FileReference )
					{
						hasFileInputs = true;
						var file : FileReference = value as FileReference;
						
						// XXX This throws an exception if browse() has not been
						// called.
						//
						try
						{
							if ( file.name != null ) fileReferences.push ( { name: name, file: file } );
						}
						catch ( e : * )
						{
						}
					}
					else
					{
						variables [ name ] = value;
					}
				}
			}
			
			// We can't upload multiple files in a single submission because of
			// Flash limitations. So if it seems like this form has any file
			// inputs then we add a few submission variables to allow the server
			// to manage the necessary multiple submissions.
			//
			
			var submissionId : String;
			
			if ( fileReferences.length > 0 )
			{
				submissionId = createSubmissionId ( );
				multiFileSubmissions [ submissionId ] = fileReferences; 
			}
			
			if ( ( hasFileInputs && gS ( "include-frontal-form-variables" ) !== false ) || gS ( "include-frontal-form-variables" ) )
			{
				if ( submissionId == null ) submissionId = createSubmissionId ( );
				variables.__frontalSubmissionID = submissionId;
				variables.__frontalSubmissionCount = fileReferences.length > 0 ? fileReferences.length : 1;
				variables.__frontalSubmissionIndex = 1;
			}
			
			var request : URLRequest = new URLRequest ( getAttribute ( "action" ) );
			request.data = variables;
			
			// XXX For some reason, this trace fixes an issue with the request
			// not working in some cases. We had two very similar forms and one
			// would get a 404 from the server and the other would not. The 
			// problem was that one would insert the "?" in the query and one 
			// would not. Adding the trace fixes the issue even when not running
			// in the IDE.
			//
			// 127.0.0.1 - - [11/May/2009:14:06:10 -0400] "GET /?input2=set&input4=6&submit1=clicked&input1=s&input3=null HTTP/1.1" 200 7404 "file:///C|/Apache2/htdocs/Frontal/tests/test%5Fassets/test.swf" "Shockwave Flash"
			// 127.0.0.1 - - [11/May/2009:14:06:35 -0400] "GET /input2=set&submit1=clicked&input1=a&input3=null&input4=6 HTTP/1.1" 404 372 "file:///C|/Apache2/htdocs/Frontal/tests/test%5Fassets/test.swf" "Shockwave Flash"
			//
			trace ( request.url );
			
			if ( getAttribute ( "method" ).toLowerCase ( ) == "post" ) request.method = URLRequestMethod.POST;
			if ( attributeExists ( "enctype" ) ) request.contentType = getAttribute ( "enctype" );
		
			value = runInteractions ( "onAction", { request: request } );
			if ( ( value !== false ) ) 
			{
				// Handle case that there is no action attribute.
				//
				if ( request.url == null || request.url.length == 0 )
				{
					runInteractions ( "onSuccess", { bytesLoaded: 0, bytesTotal: 0 } );
				}
				else
				{
					var loader : *;
					
					if ( fileReferences.length > 0 )
					{
						loader = fileReferences [ 0 ].file;
						loader.upload ( request, fileReferences [ 0 ].name );
						activeSubmissions [ loader ] = { request: request, submissionId: submissionId, bytesLoaded: 0, bytesTotal: fileReferences [ 0 ].size };
						addListener ( loader, DataEvent.UPLOAD_COMPLETE_DATA, loadDataHandler );
					}
					else
					{
						loader = new URLLoader ( request );
						activeSubmissions [ loader ] = { bytesLoaded: 0, bytesTotal: 0 };
					}
					addListener ( loader, Event.COMPLETE,						loadEventHandler );
					addListener ( loader, IOErrorEvent.IO_ERROR,				loadEventHandler );
					addListener ( loader, SecurityErrorEvent.SECURITY_ERROR,	loadEventHandler );
					addListener ( loader, ProgressEvent.PROGRESS,				formProgressHandler );
				}
			}
			
			// We need to render in case we have a loader.
			//
			if ( getStyle ( "show-progress-indicator" ) != "never" ) needsRender = true;
		}
		
		/**
		 * @private
		 */
		internal function formProgressHandler ( event : ProgressEvent ) : void
		{
			var activeSubmission : Object = activeSubmissions [ event.target ];
			if ( activeSubmission != null )
			{
				activeSubmission.bytesLoaded = event.bytesLoaded;
				activeSubmission.bytesTotal  = event.bytesTotal;
			}
			dispatchEvent ( new FrontalEvent ( FrontalEvent.PROGRESS ) );
		}
		
		/**
		 * @private
		 */
		public function loadDataHandler ( event : DataEvent ) : void
		{
			runInteractions ( "onFormData", getLoaderEventDetails ( event ) )
			removeListener ( event.target as EventDispatcher, DataEvent.UPLOAD_COMPLETE_DATA, loadEventHandler );
		}
		
		/**
		 * @private
		 */
		public function loadEventHandler ( event : Event ) : void
		{
			removeListener ( event.target as EventDispatcher, Event.COMPLETE,						loadEventHandler );
			removeListener ( event.target as EventDispatcher, IOErrorEvent.IO_ERROR,				loadEventHandler );
			removeListener ( event.target as EventDispatcher, SecurityErrorEvent.SECURITY_ERROR,	loadEventHandler );
			removeListener ( event.target as EventDispatcher, ProgressEvent.PROGRESS,				formProgressHandler );
			
			var details : Object = getLoaderEventDetails ( event );
			
			if ( event.type == Event.COMPLETE )
			{
				var isComplete : Boolean = true;
				
				if ( details.file != null )
				{
					// Are there more files to submit?
					//
					if ( details.fileReferences != null && details.submissionIndex >= 0 && details.submissionIndex < details.submissionCount )
					{
						var value : * = runInteractions ( "onPartialSuccess", details );
						if ( value !== false ) 
						{
							details.submissionIndex++;
							if ( gS ( "include-frontal-form-variables" ) !== false )
							{
								if ( ! gS ( "resend-vars-in-multi-file-uploads" ) ) details.request.data = new URLVariables ( );
								details.request.data.__frontalSubmissionID = details.submissionId;
								details.request.data.__frontalSubmissionCount = details.submissionCount;
								details.request.data.__frontalSubmissionIndex = details.submissionIndex;
							}
							
							var loader : FileReference = details.fileReferences [ details.submissionIndex - 1 ].file;
							loader.upload ( details.request, details.fileReferences [ details.submissionIndex - 1 ].name )
							activeSubmissions [ loader ] = { request: details.request, submissionId: details.submissionId, bytesLoaded: 0, bytesTotal: loader.size };
							addListener ( loader, Event.COMPLETE,						loadEventHandler );
							addListener ( loader, IOErrorEvent.IO_ERROR,				loadEventHandler );
							addListener ( loader, SecurityErrorEvent.SECURITY_ERROR,	loadEventHandler );
							addListener ( loader, ProgressEvent.PROGRESS,				formProgressHandler );
							addListener ( loader, DataEvent.UPLOAD_COMPLETE_DATA,		loadDataHandler );
							isComplete = false;
						}
					}
				}
				if ( isComplete ) 
				{
					runInteractions ( "onSuccess", details );
					if ( details.file != null ) delete multiFileSubmissions [ details.submissionId ];
				}
			}
			else
			{
				runInteractions ( "onError", details );
				if ( details.file != null ) delete multiFileSubmissions [ details.submissionId ];
			}
			
			// XXX We can't delete this here because we might still be waiting
			// for the DataEvent.UPLOAD_COMPLETE_DATA in which we'll need this
			// object. But then we don't know if we're going to receive that 
			// event at all so when do we delete this entry? For now, never.
			//
			archivedSubmissions [ event.target ] = activeSubmissions [ event.target ];
			delete activeSubmissions [ event.target ];
		}
		
		/**
		 * @private
		 */
		internal function getLoaderEventDetails ( event : Event ) : Object
		{
			var result : Object = { event: event };
			if ( event.target is FileReference ) 
			{
				result.file = event.target as FileReference;
				var submission : Object = activeSubmissions [ result.file ] != null ? activeSubmissions [ result.file ] : archivedSubmissions [ result.file ]
				result.submissionId = submission.submissionId;
				result.request = submission.request;
				
				// XXX If the submission is archived then the rest of this
				// result is historically inaccurate.
				//
				result.submissionCount = 0;
				result.submissionIndex = -1;
				result.fileReferences = multiFileSubmissions [ result.submissionId ];
				if ( result.fileReferences != null )
				{
					var index : int = -1;
					for ( var i : int = 0; i < result.fileReferences.length; i++ ) 
					{
						if ( result.fileReferences [ i ].file === result.file )
						{
							index = i;
							break;
						}
					}
					result.submissionCount = result.fileReferences.length;
					result.submissionIndex = index + 1;
				}
			}
			return result;
		}

		/**
		 * @private
		 */
		public function createSubmissionId ( ) : String
		{
			var submissionId : String = "";
			for ( var i : uint = 0; i < 10; i++ ) submissionId += String.fromCharCode ( 65 + Math.floor ( Math.random ( ) * 26 ) );
			return submissionId;
		}
		
		/**
		 * @private
		 */
		override public function reset ( ) : void 
		{
			super.reset ( );
			
			if ( focusMgr != null && getStyle ( "form-auto-focus" ) ) 
			{
				// XXX When do we deactivate the focusMgr? onDeselect?
				//
				focusMgr.activate ( );
				if ( initialFocus != null ) focusMgr.setFocus ( initialFocus.input );
			}
		}
		
		/**
		 * @private
		 */
		override public function calculateLoadProgress ( progress : Object ) : void 
		{
			var entry : Object = {
					docElem: this,
					bytesLoaded: 0,
					bytesTotal: 0,
					fraction: 0		// This is not necessarily bytesLoaded / bytesTotal because initially bytesTotal may be zero.
				};

			// First count the submissions that are running or queued. We need
			// this to calculate our fraction.
			//
			var submissionCount : uint = 0;
			for ( var loader : * in activeSubmissions )
			{
				if ( loader is FileReference )
				{
					var multiFileSubmission : Object = multiFileSubmissions [ activeSubmissions [ loader ].submissionId ];
					if ( multiFileSubmission != null ) submissionCount += multiFileSubmission.length;
				}
				else
				{
					submissionCount++;
				}
			}
			
			if ( submissionCount > 0 )
			{
				// Now total up the submissions.
				//
				for ( loader in activeSubmissions )
				{
					// First add in the active submission.
					//
					// XXX Also handle strangeness on IE where bytesLoaded 
					// is a non-zero value but bytesTotal is zero.
					//
					var submission : Object = activeSubmissions [ loader ];
					var submissionBytesTotal : Number = submission.bytesTotal == 0 && submission.bytesLoaded > 0 ? submission.bytesLoaded : submission.bytesTotal;
					entry.bytesLoaded += submission.bytesLoaded;
					entry.bytesTotal += submissionBytesTotal;
					if ( submissionBytesTotal > 0 ) entry.fraction += ( submission.bytesLoaded / submissionBytesTotal ) * ( 1 / submissionCount );
					
					// Now if this active submission is part of a multi-file
					// submission then add in the files uploaded so far as well as
					// the files to be loaded.
					//
					if ( loader is FileReference )
					{
						multiFileSubmission = multiFileSubmissions [ submission.submissionId ];
						if ( multiFileSubmission != null ) 
						{
							var gotIt : Boolean = false;
							for ( var i : uint = 0; i < multiFileSubmission.length; i++ )
							{
								if ( multiFileSubmission [ i ].file === loader )
								{
									gotIt = true;
									continue;
								}
								var bytesTotal : uint = multiFileSubmission [ i ].file.size;
								var bytesLoaded : Number = gotIt ? 0 : bytesTotal;
								entry.bytesLoaded += bytesLoaded;
								entry.bytesTotal += bytesTotal;
								if ( bytesTotal > 0 ) entry.fraction += ( bytesLoaded / bytesTotal ) * ( 1 / submissionCount );
							}
						}
					}
				}
			}
			else
			{
				entry.fraction = 1;
			}
				
			progress.entries.push ( entry );
				
			super.calculateLoadProgress ( progress );
		}
	}
}
